package com.sevenheaven.gesturelock;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.Path;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.app.Context;
import ohos.multimodalinput.event.TouchEvent;

import java.util.ArrayList;
import java.util.List;

public class GestureLock extends Component implements Component.DrawTask, Component.TouchEventListener {
    public static final int MODE_NORMAL = 0;
    public static final int MODE_EDIT = 1;

    private int mode = MODE_NORMAL;

    private int depth = 3;

    private int mWidth, mHeight;
    private int mCenterX, mCenterY;

    private int[] defaultGestures = new int[] {0};
    private int[] negativeGestures;

    private int[] gesturesContainer;
    private int gestureCursor = 0;

    private Path gesturePath;

    private int lastX;
    private int lastY;
    private int lastPathX;
    private int lastPathY;

    private static final int MAX_BLOCK_SIZE = 200;

    private int mContentSize;
    private int mHalfContentSize;
    private Paint paint;

    private int unmatchedCount;
    private int unmatchedBoundary = 5;

    private boolean touchable;

    private int DEFAULT_ERROR_COLOR = 0x66FF0000;
    private int DEFAULT_COLOR = 0x66FFFFFF;
    private int mCustomColor;
    private int mCustomErrorColor;

    private OnGestureEventListener onGestureEventListener;
    private GestureLockAdapter mAdapter;

    private List<GestureLockViewHolder> gestureLockViewHolderList = new ArrayList<>();

    public interface OnGestureEventListener {
        void onBlockSelected(int position);

        void onGestureEvent(boolean matched, int errorCount);

        void onUnmatchedExceedBoundary();

        void onGesturesFinish(int[] gestures);
    }

    /**
     * GestureLockAdapter provide a way to customize the depth, correct gestures and gesture locker style
     */
    public interface GestureLockAdapter {
        int getDepth();

        int[] getCorrectGestures();

        int getUnmatchedBoundary();

        int getBlockGapSize();

        GestureLockView getGestureLockViewInstance(Context context, int position);
    }

    public GestureLock(Context context) {
        this(context, null);
    }

    public GestureLock(Context context, AttrSet attrs) {
        this(context, attrs, null);
    }

    public GestureLock(Context context, AttrSet attrs, String defStyle) {
        super(context, attrs, defStyle);

        negativeGestures = new int[depth * depth];
        for (int i = 0; i < negativeGestures.length; i++) negativeGestures[i] = -1;
        gesturesContainer = negativeGestures.clone();

        int lineWidth = AttrUtils.getDimensionFromAttr(attrs, "line_width", dp2px(5));
        mCustomColor = AttrUtils.getColorFromAttr(attrs, "line_normal_color", DEFAULT_COLOR);
        mCustomErrorColor = AttrUtils.getColorFromAttr(attrs, "line_error_color", DEFAULT_ERROR_COLOR);

        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE_STYLE);
        paint.setStrokeWidth(lineWidth);
        paint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);
        paint.setStrokeJoin(Paint.Join.ROUND_JOIN);

        unmatchedCount = 0;
        touchable = true;
        addDrawTask(this);
        setTouchEventListener(this);
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        if (gestureLockViewHolderList != null && gestureLockViewHolderList.size() > 0) {
            calculator();
            for (int i = 0; i < gestureLockViewHolderList.size(); i++) {
                GestureLockViewHolder gestureLockViewHolder = gestureLockViewHolderList.get(i);
                if (gestureLockViewHolder != null && gestureLockViewHolder.gestureLockView != null) {
                    canvas.save();
                    canvas.translate(gestureLockViewHolder.offsetX, gestureLockViewHolder.offsetY);
                    gestureLockViewHolder.gestureLockView.draw(
                            canvas, gestureLockViewHolder.width, gestureLockViewHolder.height);
                    canvas.restore();
                }
            }
            if (gesturePath != null) {
                Path path = new Path(gesturePath);
                path.lineTo(new Point(lastX, lastY));
                canvas.drawPath(path, paint);
            }
        }
    }

    public void setAdapter(GestureLockAdapter adapter) {
        if (mAdapter == adapter) return;

        mAdapter = adapter;

        if (mAdapter != null) {
            updateParametersForAdapter();
            updateChildForAdapter();
            invalidate();
        }
    }

    public void setUnmatchedBoundary(int unmatchedBoundary) {
        this.unmatchedBoundary = unmatchedBoundary;
    }

    public int getUnmatchedBoundary() {
        return unmatchedBoundary;
    }

    public int[] getCorrectGestures() {
        return defaultGestures;
    }

    public void setCorrectGestures(int[] gesturesContainer) {
        if (gesturesContainer.length > negativeGestures.length) return;
        this.defaultGestures = gesturesContainer;
    }

    private void updateParametersForAdapter() {
        this.depth = mAdapter.getDepth();

        negativeGestures = new int[depth * depth];
        for (int i = 0; i < negativeGestures.length; i++) negativeGestures[i] = -1;
        gesturesContainer = negativeGestures.clone();
        defaultGestures = mAdapter.getCorrectGestures();

        if (defaultGestures.length > negativeGestures.length)
            throw new IllegalArgumentException(
                    "defaultGestures length must be less than or equal to " + negativeGestures.length);

        unmatchedBoundary = mAdapter.getUnmatchedBoundary();
    }

    private void updateChildForAdapter() {
        final int totalBlockCount = depth * depth;
        gestureLockViewHolderList = new ArrayList<>();

        for (int i = 0; i < totalBlockCount; i++) {
            GestureLockViewHolder gestureLockViewHolder = new GestureLockViewHolder();
            gestureLockViewHolder.gestureLockView = mAdapter.getGestureLockViewInstance(getContext(), i);
            gestureLockViewHolder.gestureLockView.setLockerState(GestureLockView.LockerState.LOCKER_STATE_NORMAL);
            gestureLockViewHolder.index = i;
            gestureLockViewHolderList.add(gestureLockViewHolder);
        }
    }

    public void notifyDataChanged() {
        updateParametersForAdapter();
        updateChildForAdapter();
        invalidate();
    }

    public void setEditMode(int mode) {
        clear();
        this.mode = mode;
    }

    public int getEditMode() {
        return mode;
    }

    public void setTouchable(boolean touchable) {
        this.touchable = touchable;
    }

    public void resetUnmatchedCount() {
        unmatchedCount = 0;
    }

    public void setOnGestureEventListener(OnGestureEventListener onGestureEventListener) {
        this.onGestureEventListener = onGestureEventListener;
    }

    private class GestureLockViewHolder {
        GestureLockView gestureLockView;
        int index;
        int width;
        int height;
        int offsetX;
        int offsetY;
        int centerX;
        int centerY;
    }

    private void calculator() {
        mWidth = getWidth();
        mHeight = getHeight();
        mCenterX = mWidth / 2;
        mCenterY = mHeight / 2;
        mContentSize = mWidth > mHeight ? mHeight : mWidth;
        mHalfContentSize = mContentSize / 2;
        int totalGap = mAdapter.getBlockGapSize() * (depth - 1);

        final int xStart = mCenterX - mHalfContentSize;
        final int yStart = mCenterY - mHalfContentSize;

        int xStep = xStart;
        int yStep = yStart;

        int childSize = (mContentSize - totalGap) / depth;
        if (gestureLockViewHolderList != null && gestureLockViewHolderList.size() > 0) {
            for (int i = 0; i < gestureLockViewHolderList.size(); i++) {
                GestureLockViewHolder gestureLockViewHolder = gestureLockViewHolderList.get(i);
                if (gestureLockViewHolder != null) {
                    gestureLockViewHolder.width = childSize;
                    gestureLockViewHolder.height = childSize;
                    gestureLockViewHolder.offsetX = xStep;
                    gestureLockViewHolder.offsetY = yStep;
                    gestureLockViewHolder.centerX = gestureLockViewHolder.offsetX + childSize / 2;
                    gestureLockViewHolder.centerY = gestureLockViewHolder.offsetY + childSize / 2;
                    if (i % depth == depth - 1) {
                        xStep = xStart;
                        yStep += childSize + mAdapter.getBlockGapSize();
                    } else {
                        xStep += childSize + mAdapter.getBlockGapSize();
                    }
                }
            }
        }
    }

    public void clear() {
        for (int i = 0; i < gestureLockViewHolderList.size(); i++) {
            GestureLockViewHolder gestureLockViewHolder = gestureLockViewHolderList.get(i);
            if (gestureLockViewHolder.gestureLockView != null) {
                gestureLockViewHolder.gestureLockView.setLockerState(GestureLockView.LockerState.LOCKER_STATE_NORMAL);
                gestureLockViewHolder.gestureLockView.setArrow(-1);
            }
        }
        gesturePath = null;
        invalidate();
    }

    private int touchIndex(float x, float y) {
        if (gestureLockViewHolderList != null && gestureLockViewHolderList.size() > 0) {
            for (int i = 0; i < gestureLockViewHolderList.size(); i++) {
                GestureLockViewHolder gestureLockViewHolder = gestureLockViewHolderList.get(i);
                if (x > gestureLockViewHolder.offsetX + gestureLockViewHolder.width * 0.2f
                        && x < gestureLockViewHolder.offsetX + gestureLockViewHolder.width * 0.8f
                        && y > gestureLockViewHolder.offsetY + gestureLockViewHolder.height * 0.2f
                        && y < gestureLockViewHolder.offsetY + gestureLockViewHolder.height * 0.8f) {
                    return gestureLockViewHolder.index;
                }
            }
        }
        return -1;
    }

    @Override
    public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
        if (gestureLockViewHolderList == null || !touchable) return false;
        if (touchEvent.getPointerCount() == 1 && touchEvent.getAction() == TouchEvent.PRIMARY_POINT_DOWN) {
            for (GestureLockViewHolder gestureLockViewHolder : gestureLockViewHolderList) {
                if (gestureLockViewHolder != null && gestureLockViewHolder.gestureLockView != null) {
                    gestureLockViewHolder.gestureLockView.setLockerState(
                            GestureLockView.LockerState.LOCKER_STATE_NORMAL);
                    gestureLockViewHolder.gestureLockView.setArrow(-1);
                }
            }
            gesturePath = null;
            lastX = (int) getTouchX(touchEvent, 0);
            lastY = (int) getTouchY(touchEvent, 0);
            lastPathX = lastX;
            lastPathY = lastY;
            paint.setColor(new Color(mCustomColor));
            invalidate();
        } else if (touchEvent.getPointerCount() == 1 && touchEvent.getAction() == TouchEvent.POINT_MOVE) {
            lastX = (int) getTouchX(touchEvent, 0);
            lastY = (int) getTouchY(touchEvent, 0);
            int index = touchIndex(lastX, lastY);
            boolean checked = false;
            for (int id : gesturesContainer) {
                if (id == index) {
                    checked = true;
                    break;
                }
            }
            if (!checked && index != -1) {
                if (gestureLockViewHolderList != null && gestureLockViewHolderList.size() > 0) {
                    for (GestureLockViewHolder gestureLockViewHolder : gestureLockViewHolderList) {
                        if (gestureLockViewHolder.index == index) {
                            if (gesturePath == null) {
                                gesturePath = new Path();
                                gesturePath.moveTo(
                                        gestureLockViewHolder.centerX, gestureLockViewHolder.centerY);
                            } else {
                                gesturePath.lineTo(
                                        gestureLockViewHolder.centerX, gestureLockViewHolder.centerY);
                            }
                            gesturesContainer[gestureCursor] = gestureLockViewHolder.index;
                            if (gestureCursor > 0) {
                                GestureLockViewHolder last =
                                        getGestureLockViewHolderFromIndex(
                                                gesturesContainer[gestureCursor - 1]);
                                int dx = gestureLockViewHolder.centerX - last.centerX;
                                int dy = gestureLockViewHolder.centerY - last.centerY;

                                int angle = (int) Math.toDegrees(Math.atan2(dy, dx)) + 90;
                                if (last.gestureLockView != null) {
                                    last.gestureLockView.setArrow(angle);
                                }
                            }
                            gestureCursor++;
                            lastPathX = gestureLockViewHolder.centerX;
                            lastPathY = gestureLockViewHolder.centerY;
                            if (gestureLockViewHolder.gestureLockView != null) {
                                gestureLockViewHolder.gestureLockView.setLockerState(
                                        GestureLockView.LockerState.LOCKER_STATE_SELECTED);
                            }

                            if (onGestureEventListener != null)
                                onGestureEventListener.onBlockSelected(gestureLockViewHolder.index);
                        }
                    }
                }
            }
            invalidate();
        } else if (touchEvent.getPointerCount() == 1
                && touchEvent.getAction() == TouchEvent.PRIMARY_POINT_UP) {
            if (gesturesContainer[0] != -1) {
                boolean matched = false;

                if (gesturesContainer.length == defaultGestures.length
                        || gesturesContainer[defaultGestures.length] == -1) {
                    for (int j = 0; j < defaultGestures.length; j++) {
                        if (gesturesContainer[j] == defaultGestures[j]) {
                            matched = true;
                        } else {
                            matched = false;
                            break;
                        }
                    }
                }

                if (!matched && mode != MODE_EDIT) {
                    unmatchedCount++;
                    paint.setColor(new Color(mCustomErrorColor));
                    for (int k = 0; k < gesturesContainer.length; k++) {
                        GestureLockViewHolder gestureLockViewHolder =
                                getGestureLockViewHolderFromIndex(gesturesContainer[k]);
                        if (gestureLockViewHolder != null) {
                            gestureLockViewHolder.gestureLockView.setLockerState(
                                    GestureLockView.LockerState.LOCKER_STATE_ERROR);
                            if (k < gesturesContainer.length - 1 && gesturesContainer[k + 1] != -1) {
                                GestureLockViewHolder nextChild =
                                        getGestureLockViewHolderFromIndex(gesturesContainer[k + 1]);
                                if (nextChild != null) {
                                    int dx = nextChild.centerX - gestureLockViewHolder.centerX;
                                    int dy = nextChild.centerY - gestureLockViewHolder.centerY;

                                    int angle = (int) Math.toDegrees(Math.atan2(dy, dx)) + 90;
                                    gestureLockViewHolder.gestureLockView.setArrow(angle);
                                }
                            }
                        }
                    }
                } else {
                    unmatchedCount = 0;
                }

                if (onGestureEventListener != null) {
                    onGestureEventListener.onGestureEvent(matched, unmatchedCount);
                    if (unmatchedCount >= unmatchedBoundary) {
                        onGestureEventListener.onUnmatchedExceedBoundary();
                        unmatchedCount = 0;
                    }
                    if (gestureCursor > 0) {
                        int[] finishGestures = new int[gestureCursor];
                        for (int i = 0;
                             i < gestureCursor && gestureCursor < gesturesContainer.length;
                             i++) {
                            finishGestures[i] = gesturesContainer[i];
                        }

                        onGestureEventListener.onGesturesFinish(finishGestures);
                    }
                }
            }
            gestureCursor = 0;
            gesturesContainer = negativeGestures.clone();

            lastX = lastPathX;
            lastY = lastPathY;

            invalidate();
        }
        return true;
    }

    private GestureLockViewHolder getGestureLockViewHolderFromIndex(int index) {
        if (gestureLockViewHolderList != null && gestureLockViewHolderList.size() > 0) {
            for (GestureLockViewHolder gestureLockViewHolder : gestureLockViewHolderList) {
                if (gestureLockViewHolder.index == index) {
                    return gestureLockViewHolder;
                }
            }
        }
        return null;
    }

    private float getTouchX(TouchEvent touchEvent, int index) {
        float x = 0;
        if (touchEvent.getPointerCount() > index) {
            int[] xy = getLocationOnScreen();
            if (xy != null && xy.length == 2) {
                x = touchEvent.getPointerScreenPosition(index).getX() - xy[0];
            } else {
                x = touchEvent.getPointerPosition(index).getX();
            }
        }
        return x;
    }

    private float getTouchY(TouchEvent touchEvent, int index) {
        float y = 0;
        if (touchEvent.getPointerCount() > index) {
            int[] xy = getLocationOnScreen();
            if (xy != null && xy.length == 2) {
                y = touchEvent.getPointerScreenPosition(index).getY() - xy[1];
            } else {
                y = touchEvent.getPointerPosition(index).getY();
            }
        }
        return y;
    }

    private int dp2px(float dp) {
        return (int) (getResourceManager().getDeviceCapability().screenDensity / 160 * dp);
    }
}
